// vue指令文档 https://vuejs.org/guide/reusability/custom-directives.html
// 指令钩子：一般处理updated就能捕获所有的参数变化
// 但是如果指令有自定义增删DOM元素，就需要mounted和unmounted来增加和删除增删的DOM元素
// 因为v-if可能会让绑定了指定的元素触发mounted和unmounted
// 指令钩子执行注意事项：
// 首次会执行mounted和updated
// v-if变为false会执行unmounted
// v-if变为true仅会执行mounted
// 指令名默认文件名，例如loading.js，指令就是v-loading
// binding参数：(v-ee为示例指令)
// arg：使用':'，声明v-ee:arg / v-ee:[arg]，调用binding.arg
// modifiers：使用'.'，声明v-eeui.a.b，调用binding.modifiers.a || binding.modifiers.b
// value：可以是任意类型值，声明v-ee="value"，调用binding.value
// instance：使用指令的DOM元素所在组件实例

// 在ee-ui中，指令的主要用法就是绑定updated钩子
// 使用指令的参数放入el.__[指令名]
// 使用方法：
// import d from '../ee-directive'
// export default d()
import { Directive, DirectiveHook, DirectiveBinding } from 'vue'

/** 组件umounted是否需要执行updated
 * 如果需要执行，则可以根据指令函数的参数定制新的参数以代表是umounted
 * @typedef {DirectiveHook} delete
 * @example
 * 例如placeholder在元素前插入了占位的div元素，unmounted时需要将占位div删除
 * directive({
 *   name: 'placeholder',
 *   // 函数将binding.value设置为0代表删除placeholder
 *   delete: (el, binding, vnode, prevVnode) => binding.value = 0,
 *   // ... 其它代码
 * })
 */

/** 指令扩展配置
 * @typedef {object} ext
 * @property {string} name - 指令的名字
 * @property {delete} [delete] - unmounted时执行一次updated函数，传参全为空，自定义unmounted时次参数无效
 * @property {DirectiveHook} [init] - 首次初始化时调用的函数
 * @property {DirectiveHook} [change] - binding参数深度对比有不同时操作的函数
 * @property {DirectiveHook} [always] - 不论什么情况总是要执行的函数
 */

/** 数据扩展
 * @typedef {object} data
 * @property {{arg:string, value}} clone - 克隆的binding数据，用于判断指令参数是否发生变化
 * @property {DirectiveBinding} origin - 原始的binding数据
 */

/** 克隆一个binding，包含arg和value
 * modifiers在模板上使用时是不允许变化的所以没有克隆(理论上渲染时可以赋值)
 * @param {DirectiveBinding} binding
 * @returns {DirectiveBinding}
 */
function cloneBinding(binding) {
  const clone = { arg: binding.arg, value: binding.value };
  const value = clone.value;
  if (value?.constructor == Object)
    clone.value = { ...value };
  return clone;
}

/** 对比两次binding是否有发生变化，主要对比arg和value
 * @param {DirectiveBinding} x
 * @param {DirectiveBinding} y
 * @returns {boolean} 是否有不同
 */
function compare(x, y) {
  if (compareArray(x.arg, y.arg))
    return true;
  const x1 = x.value;
  const y1 = y.value;
  if (x1 == y1)
    return false;
  const o1 = x1?.constructor == Object;
  const o2 = y1?.constructor == Object;
  if (o1 != o2)
    return true;
  if (o1 && o2) {
    // value都是object则深度对比
    for (const key in x1)
      if (compareArray(x1[key], y1[key]))
        return true;
    return false;
  }
  return true;
}
function compareArray(x, y) {
  if (x == y)
    return false;
  if (x?.constructor == Array && y?.constructor == Array) {
    if (x.length != y.length)
      return false;
    for (let i = 0; i < x.length; i++)
      if (x[i] != y[i])
        return true;
    return false;
  }
  return true;
}

/** 定义一个指令
 * @param {Directive | ext} d
 * 
 * @example
 * import directive from '../ee-directive.js'
 * export default directive({
 *  
 * })
 */
export function directive(d) {
  const name = '__' + d.name;
  /** @type {DirectiveHook<HTMLElement>} */
  const fd = (el, binding, vnode, prevVnode) => {
    /** @type {data} */
    let data = el[name];
    if (!data) {
      data = { clone: {} };
      el[name] = data;
      d.init?.(el, binding, vnode, prevVnode);
    }
    data.origin = binding;

    if (compare(data.clone, binding))
      d.change?.(el, binding, vnode, prevVnode);
    
    d.always?.(el, binding, vnode, prevVnode);
    // 拷贝binding.value后面用于深度比对数据，数据有变化时才执行操作
    data.clone = cloneBinding(binding);
  }

  if (!d.mounted)
    d.mounted = fd;
  if (!d.updated)
    d.updated = fd;
  if (!d.unmounted && d.delete)
    d.unmounted = function (el, binding, vnode, prevVnode) {
      d.delete(el, binding, vnode, prevVnode);
      fd(el, binding, vnode, prevVnode);
    }
      
  
  return d;
}